package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.RedisData;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RabbitTemplate mqTemplate;
    @Override
    public Result queryById(Long id) {
        //缓存穿透
//        Shop shop = queryWithId(id);
        Shop shop = queryWithPassThroygh(id);
//        Shop shop = queryWithLogicExpire(id, 3000L); //id, 过期时间为5分钟
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);

    }
    //TODO:4.解决缓存击穿问题（互斥锁）
    private Shop queryWithMutex(long id) {
        Shop shop = null;
        String cacheKey = CACHE_SHOP_KEY + id;
        String lockKey = LOCK_SHOP_KEY + id;

        try {
            // 1. 第一次查询缓存
            String shopJson = stringRedisTemplate.opsForValue().get(cacheKey);

            // 2. 缓存命中（有效数据）→ 直接返回
            if (StrUtil.isNotBlank(shopJson)) {
                log.info("缓存中包含数据，直接返回");
                return JSONUtil.toBean(shopJson, Shop.class);
            }

            // 3. 缓存是“空值占位符”→ 返回 null
            if ("".equals(shopJson)) {
                log.info("缓存中是空值占位符，返回空数据");
                return null;
            }

            // 4. 缓存未命中 → 尝试获取锁
            boolean isLocked = tryLock(lockKey);
            if (!isLocked) {
                Thread.sleep(100);
                return queryWithMutex(id); // 重试
            }

            // 5. 【Double Check】获取锁后，再次查询缓存
            shopJson = stringRedisTemplate.opsForValue().get(cacheKey);
            if (StrUtil.isNotBlank(shopJson)) { // 缓存已存在（其他线程已写入）
                log.info("Double Check：缓存已存在，直接返回");
                return JSONUtil.toBean(shopJson, Shop.class);
            }
            if ("".equals(shopJson)) { // 空值占位符
                log.info("Double Check：空值占位符，返回空数据");
                return null;
            }

            // 6. 缓存仍未命中 → 查询数据库
            shop = getById(id);
            log.info("缓存未命中，查询数据库");

            // 7. 数据库无数据 → 写入空值占位符
            if (shop == null) {
                stringRedisTemplate.opsForValue().set(
                        cacheKey,
                        "",
                        CACHE_NULL_TTL,
                        TimeUnit.MINUTES
                );
                return null;
            }
            // 8. 数据库有数据 → 写入缓存（原子操作：set + 过期时间）
            stringRedisTemplate.opsForValue().set(
                    cacheKey,
                    JSONUtil.toJsonStr(shop),
                    CACHE_SHOP_TTL,
                    TimeUnit.MINUTES
            );

        } catch (InterruptedException e) {
            throw new RuntimeException("线程休眠被中断", e);
        } finally {
            unLock(lockKey); // 释放锁
        }
        return shop;
    }
    //4


    //TODO:3.解决缓存穿透问题
    private Shop queryWithPassThroygh(Long id){
        //查询Redis缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //判段缓存中是否包含数据
        //由于我们会预热数据，所以没找到就是没有着条数据！
        if (StrUtil.isNotBlank(shopJson)) { //缓存中包含数据
            return  JSONUtil.toBean(shopJson, Shop.class);
        }
// 3. 缓存是“空值占位符”→ 返回 null
        if ("".equals(shopJson)) {
            log.info("缓存中是空值占位符，返回空数据");
            return null;
        }
        //缓存中不包含数据，查询数据库
        Shop shop = getById(id);
        log.info("缓存中不包含数据，查询数据库");
        if (shop == null) {
            //缓存穿透，设置空值缓存
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //存在将数据放置到缓存中
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop));
        //设置过期时间
        stringRedisTemplate.expire(CACHE_SHOP_KEY + id, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //返回数据
        return shop;
    }
    //创建线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    //TODO:5.解决缓存击穿问题（逻辑过期）
    private Shop queryWithLogicExpire(Long id, Long expireSeconds) {
        //查询Redis缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //判断缓存中是否包含数据
        if (StrUtil.isBlank(shopJson)) { //缓存未命中
            log.info("缓存中数据未命中");
            return null;
        }
        /**
         * 如果存在，判断是否逻辑过期
         * 1.将数据反序列化
         * 2.判断是否逻辑过期
         * 3.如果过期，重新查询数据库
         * 4.如果未过期，直接返回
         */
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //判断是否逻辑过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //未过期，直接返回
            log.info("缓存中数据未过期，直接返回");
            return shop;
        }
        //逻辑过期，重新查询数据库
        log.info("缓存中数据已过期，重新查询数据库");
        //获取锁
        boolean isLocked = tryLock(LOCK_SHOP_KEY + id);
        //判断获取锁成功
        if (!isLocked) {
            //返回旧数据
            log.info("获取锁失败，返回旧数据");
            return shop;
        }
        //获取锁成功，开启新的线程，并查询数据和写入缓存
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                //查询数据库
                Shop newShop = getById(id);
                if (newShop != null) {
                    //构建逻辑过期的RedisData
                    RedisData newRedisData = new RedisData();
                    newRedisData.setData(newShop);
                    newRedisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
                    //存在将数据放置到缓存中
                    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(newRedisData));
                } else {
                    //数据库中也没有，设置空值缓存
                    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                //释放锁
                unLock(LOCK_SHOP_KEY + id);
            }
        });
        //返回旧数据
        return shop;
    }

    @Override
    //TODO:4.开启事务
    @Transactional
    //TODO:5.解决缓存一致性（双删除策略：先删除，操作数据库，在删除）
    public Result updateShop(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店铺id不能为空");
        }
        String cacheKey = CACHE_SHOP_KEY + id;

        try {
            // 1. 第一次删缓存：失效旧缓存（避免更新前的读请求写入旧数据）
            stringRedisTemplate.delete(cacheKey);

            // 2. 更新数据库（核心操作，事务保证）
            boolean isSuccess = updateById(shop);
            if (!isSuccess) {
                return Result.fail("更新店铺失败");
            }

            // 3. 第二次删缓存：兜底（解决“第一次删缓存失败”或“主从同步延迟”问题）
            stringRedisTemplate.delete(cacheKey);
        } catch (Exception e) {
            // 4. 第二次删缓存失败：用MQ异步重试（避免数据不一致）
            log.error("更新店铺后删除缓存失败，shopId:{}", id, e);
            byte[] body = cacheKey.getBytes(); // 字符串转字节数组
            Message message = new Message(body, new MessageProperties()); // 构建 Message 对象
            mqTemplate.send("cache:delete:retry", message);
            throw new RuntimeException("更新店铺异常");
        }

        return Result.ok();
    }

    //定义两个方法获取锁和释放锁（使用setnx来实现）
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1",10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);//判断是否成功
    }
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }
    @RabbitListener(queues = "cache:delete:retry")
    public void handleCacheDeleteRetry(String cacheKey) {
        int retryCount = 0;
        // 最多重试3次，每次间隔递增（退避策略，避免Redis压力）
        while (retryCount < 3) {
            try {
                stringRedisTemplate.delete(cacheKey);
                log.info("重试删除缓存成功，key:{}", cacheKey);
                break;
            } catch (Exception e) {
                retryCount++;
                log.error("第{}次重试删除缓存失败，key:{}", retryCount, cacheKey, e);
                try {
                    Thread.sleep(100 * retryCount); // 100ms → 200ms → 300ms
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
//    // 在服务启动后，手动调用一次，将店铺数据写入缓存
//    @PostConstruct
//    public void preheatShopCache() {
//        Long shopId = 1L;
//        Shop shop = getById(shopId);
//        RedisData redisData = new RedisData();
//        redisData.setData(shop);
//        redisData.setExpireTime(LocalDateTime.now().plusMinutes(5)); // 5分钟后过期
//        stringRedisTemplate.opsForValue().set(
//                CACHE_SHOP_KEY + shopId,
//                JSONUtil.toJsonStr(redisData)
//        );
//    }
}
